﻿#region header

//  ***************************************************************************
// *                                LICENSE
//  ***************************************************************************
//  * "Haplous" .NET Simple 2D Game Framework
//  * 
//  * Copyright © Jonathan Evans 2014-2014
//  * 
//  * This program is free software; you can redistribute it and/or
//  * modify it under the terms of the GNU General Public License
//  * as published by the Free Software Foundation; either version 2
//  * of the License, or (at your option) any later version.
//  * 
//  * This program is distributed in the hope that it will be useful,
//  * but WITHOUT ANY WARRANTY; without even the implied warranty of
//  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  * GNU General Public License for more details.
//  * 
//  * You should have received a copy of the GNU General Public License
//  * along with this program; if not, write to the Free Software
//  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//  * 
//  ***************************************************************************
//  * 
//  * Contact: opencollar@googlemail.com
//  * 
//  * Website: http://haplous.codeplex.com/
//  *
//  * NuGet:   http://www.nuget.org/packages/haplous/
//  *
//  * File:    Haplous/Haplous/Game.cs
//  *
//  ***************************************************************************

#endregion

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Threading;

using Haplous.Core;
using Haplous.Correctness;

using JetBrains.Annotations;

using Dispatcher = Haplous.Core.Dispatcher;

namespace Haplous
{
    /// <summary>
    ///     The arguments used for the <see cref="Game.MessageSent" /> event.
    /// </summary>
    [DebuggerDisplay("MessageSentEventArgs: Message={_message}")]
    public class MessageSentEventArgs : EventArgs
    {
        /// <summary>
        ///     The message sent.
        /// </summary>
        [NotNull]
        private readonly IMessage _message;

        /// <summary>
        ///     Initializes a new instance of the <see cref="MessageSentEventArgs" /> class.
        /// </summary>
        /// <param name="message">The message sent.</param>
        public MessageSentEventArgs([NotNull] IMessage message)
        {
            _message = message;
        }

        /// <summary>
        ///     The message sent.
        /// </summary>
        [NotNull]
        public IMessage Message
        {
            get { return _message; }
        }
    }

    /// <summary>
    ///     The base class for game controllers that define all aspects of a game.
    /// </summary>
    [DebuggerDisplay("Game: Name={Name}; Size={_size.Width),{_size.Height)")]
    public abstract class Game : DisposableNotifyObject
    {
        /// <summary>
        ///     A dictionary of all the synchronizers that will be available to use in this game.
        /// </summary>
        [NotNull]
        private readonly RootDictionary<Cue> _cues = new RootDictionary<Cue>("Cues");

        /// <summary>
        ///     A dictionary of all the layers that will be available to use in this game.
        /// </summary>
        [NotNull]
        private readonly RootDictionary<Layer> _layers = new RootDictionary<Layer>("Layers");

        /// <summary>
        ///     A lock used to control concurrent access to the <see cref="_mode" /> field.
        /// </summary>
        [NotNull]
        private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

        /// <summary>
        ///     A dictionary of all the modes that will be available to use in this game.
        /// </summary>
        [NotNull]
        private readonly RootDictionary<Mode> _modes = new RootDictionary<Mode>("Modes");

        /// <summary>
        ///     A dictionary of all the pictures that will be available to use in this game.
        /// </summary>
        [NotNull]
        private readonly RootDictionary<PictureSequence> _pictureSequences = new RootDictionary<PictureSequence>("PictureSequences");

        /// <summary>
        ///     A dictionary of all the pictures that will be available to use in this game.
        /// </summary>
        [NotNull]
        private readonly RootDictionary<Picture> _pictures = new RootDictionary<Picture>("Pictures");

        /// <summary>
        ///     The size of the game area (in nominal pixels) - defaults to SVGA.
        /// </summary>
        [NotNull]
        private readonly SizeInt32 _size = new SizeInt32(1024, 768);

        /// <summary>
        ///     The flag that controls whether or not the game is running.
        /// </summary>
        private bool _isPlaying;

        /// <summary>
        ///     The active game mode.
        /// </summary>
        [CanBeNull]
        private Mode _mode;

        /// <summary>
        ///     Initializes a new instance of the <see cref="Game" /> class.
        /// </summary>
        protected Game()
        {
            Application.Current.DispatcherUnhandledException += UnhandledDispatcherException;
            Application.Current.Exit += ApplicationExit;
        }

        /// <summary>
        ///     The flag that controls whether or not the game is running.
        /// </summary>
        public bool IsPlaying
        {
            get { return _isPlaying; }
        }

        /// <summary>
        ///     The size of the game area (in nominal pixels) - defaults to SVGA.
        /// </summary>
        [NotNull]
        public SizeInt32 Size
        {
            get { return _size; }
        }

        /// <summary>
        ///     Gets the name of this game (used internally in error messages etc.).
        /// </summary>
        /// <value>The name of the game.</value>
        [NotNull]
        public abstract string Name { get; }

        /// <summary>
        ///     A dictionary of all the modes that will be available to use in this game.
        /// </summary>
        [NotNull]
        public RootDictionary<Mode> Modes
        {
            get { return _modes; }
        }

        /// <summary>
        ///     A dictionary of all the layers that will be available to use in this game.
        /// </summary>
        [NotNull]
        public RootDictionary<Layer> Layers
        {
            get { return _layers; }
        }

        /// <summary>
        ///     A dictionary of all the pictures that will be available to use in this game.
        /// </summary>
        [NotNull]
        public RootDictionary<Picture> Pictures
        {
            get { return _pictures; }
        }

        /// <summary>
        ///     A dictionary of all the picture sequences that will be available to use in this game.
        /// </summary>
        [NotNull]
        public RootDictionary<PictureSequence> PictureSequences
        {
            get { return _pictureSequences; }
        }

        /// <summary>
        ///     A dictionary of all the synchronizers that will be available to use in this game.
        /// </summary>
        [NotNull]
        public RootDictionary<Cue> Cues
        {
            get { return _cues; }
        }

        /// <summary>
        ///     Gets or sets the active mode which governs the resources and configuration of the game.
        /// </summary>
        /// <value>The active mode.</value>
        /// <exception cref="Haplous.Core.UnknownResourceException">
        ///     Occurs when the active mode is set to a value that does not
        ///     appear in the <see cref="Modes" /> dictionary.
        /// </exception>
        [NotNull]
        public Mode ActiveMode
        {
            get { return _mode; }
            set
            {
                value.ValidateArgument("value");

                if(!_modes.Contains(value))
                    throw new UnknownResourceException(string.Format(CultureInfo.InvariantCulture, "The mode specified has not be registered in the Game.Modes dictionary: \"{0}\".", value.Name));

                Mode previousMode;

                _lock.EnterUpgradeableReadLock();
                try
                {
                    previousMode = _mode;

                    if(value == previousMode)
                        return;

                    OnModeChanging(previousMode, value);

                    _lock.EnterWriteLock();
                    try
                    {
                        _mode = value;
                        Debug.WriteLine("Setting mode: " + value.Name);
                    }
                    finally
                    {
                        _lock.ExitWriteLock();
                    }
                }
                finally
                {
                    _lock.ExitUpgradeableReadLock();
                }

                OnModeChanged(previousMode, value);

                OnPropertyChanged("ActiveMode");

                Debug.WriteLine("Mode set: " + value.Name);
            }
        }

        /// <summary>
        ///     Gets all the elements defined in the game (across all layers).
        /// </summary>
        /// <value>The elements defined.</value>
        [NotNull]
        public IEnumerable<IElement> Elements
        {
            get { return _layers.SelectMany(layer => layer.Elements); }
        }

        /// <summary>
        ///     Copies the property values for all elements in one mode to another mode.
        /// </summary>
        /// <param name="source">The source mode of the property values.</param>
        /// <param name="destination">The destination mode of the property values.</param>
        /// <param name="excludedProperties">A list of the names of the properties to exclude from the copy.</param>
        public void CopyModeProperties([NotNull] Mode source, [NotNull] Mode destination, [CanBeNull] IEnumerable<string> excludedProperties)
        {
            foreach(var element in Elements)
            {
                Debug.Assert(element != null, "element != null");
                element.CopyModeProperties(source, destination, excludedProperties);
            }
        }

        /// <summary>
        ///     Called when the application exits.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="ExitEventArgs" /> instance containing the event data.</param>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void ApplicationExit([NotNull] object sender, [NotNull] ExitEventArgs e)
        {
            try
            {
                Stop();
            }
            catch(Exception ex)
            {
                ErrorManager.OnUnhandledException(ex);
            }
        }

        /// <summary>
        ///     Called when an unhandled exception occurs in the dispatcher.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="DispatcherUnhandledExceptionEventArgs" /> instance containing the event data.</param>
        private void UnhandledDispatcherException([CanBeNull] object sender, [NotNull] DispatcherUnhandledExceptionEventArgs e)
        {
            ErrorManager.OnUnhandledException(e.Exception);
            e.Handled = true;
        }

        /// <summary>
        ///     Occurs when a message has been sent.
        /// </summary>
        public event EventHandler<MessageSentEventArgs> MessageSent;

        /// <summary>
        ///     Sends a message for all <see cref="ElementBase" />-derived objects to receive.
        /// </summary>
        /// <param name="message">The message to send.</param>
        public void SendMessage([NotNull] IMessage message)
        {
            OnMessageSent(message);
        }

        /// <summary>
        ///     Called when a message is to be sent.
        /// </summary>
        /// <param name="message">The message.</param>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void OnMessageSent([NotNull] IMessage message)
        {
            message.ValidateArgument("message");

            var handler = MessageSent;
            if(handler == null)
                return;

            var callbacks = handler.GetInvocationList();
            if(callbacks.Length <= 0)
                return;

            var args = new MessageSentEventArgs(message);

            foreach(var callback in callbacks)
            {
                Debug.Assert(callback != null, "callback != null");

                try
                {
                    callback.DynamicInvoke(this, args);
                }
                catch(Exception ex)
                {
                    ErrorManager.OnUnhandledException(ex);
                }
            }
        }

        /// <summary>
        ///     Fires the <see cref="ModeChanged" /> event after the mode has been changed.
        /// </summary>
        /// <param name="previousMode">The previous mode.</param>
        /// <param name="newMode">The new mode.</param>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void OnModeChanged([CanBeNull] Mode previousMode, [NotNull] Mode newMode)
        {
            var callbacks = ModeChanged;
            if(callbacks == null)
                return;
            var handlers = callbacks.GetInvocationList();
            if(handlers.Length <= 0)
                return;
            var args = new ModeChangeEventArgs(previousMode, newMode);

            foreach(var handler in handlers)
            {
                try
                {
                    Debug.Assert(handler != null, "handler != null");
                    handler.DynamicInvoke(this, args);
                }
                catch(Exception ex)
                {
                    ErrorManager.OnUnhandledException(ex);
                }
            }
        }

        /// <summary>
        ///     Fires the <see cref="ModeChanging" /> event immediately before the mode is changed.
        /// </summary>
        /// <param name="previousMode">The previous mode.</param>
        /// <param name="newMode">The new mode.</param>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void OnModeChanging([CanBeNull] Mode previousMode, [NotNull] Mode newMode)
        {
            var callbacks = ModeChanging;
            if(callbacks == null)
                return;
            var handlers = callbacks.GetInvocationList();
            if(handlers.Length <= 0)
                return;
            var args = new ModeChangeEventArgs(previousMode, newMode);

            foreach(var handler in handlers)
            {
                try
                {
                    Debug.Assert(handler != null, "handler != null");
                    handler.DynamicInvoke(this, args);
                }
                catch(Exception ex)
                {
                    ErrorManager.OnUnhandledException(ex);
                }
            }
        }

        /// <summary>
        ///     Switches the game into playing mode.
        /// </summary>
        /// <remarks>
        ///     <para>Calling this method will start the controllers and synchronizers that drive the game.</para>
        ///     <para>
        ///         When this method is called the <see cref="Starting" /> and <see cref="Started" /> event and
        ///         <see cref="INotifyPropertyChanged.PropertyChanged" /> event will fire.
        ///     </para>
        ///     <para>Calling this method will have no effect if the game is already playing</para>
        ///     .
        /// </remarks>
        /// .
        public void Start()
        {
            Debug.WriteLine("Starting: " + Name);

            _lock.EnterUpgradeableReadLock();
            try
            {
                if(_isPlaying)
                    return;

                _lock.EnterWriteLock();
                try
                {
                    _isPlaying = true;
                }
                finally
                {
                    _lock.ExitWriteLock();
                }
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();
            }

            Dispatcher.Invoke(() =>
                              {
                                  OnStarting();
                                  StartResources();
                                  OnStarted();
                                  OnPropertyChanged("IsPlaying");
                              });
            Debug.WriteLine("Started: " + Name);
        }

        /// <summary>
        ///     Starts the resources that require triggering.
        /// </summary>
        private void StartResources()
        {
            Dispatcher.ValidateDispatcher(DispatcherValidationKind.MustBeOnDispatcher);

            foreach(var synchronizer in _cues.ToArray())
            {
                Debug.Assert(synchronizer != null, "Cue != null");
                synchronizer.Start();
            }

            foreach(var sequence in _pictureSequences)
            {
                Debug.Assert(sequence != null, "sequence != null");
                sequence.Initialize();
            }
        }

        /// <summary>
        ///     Occurs when the game is starting to play.
        /// </summary>
        public event EventHandler Starting;

        /// <summary>
        ///     Called before the <see cref="IsPlaying" /> property changes to <see langword="true" /> and before the resources are
        ///     notified.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        protected void OnStarting()
        {
            Dispatcher.ValidateDispatcher(DispatcherValidationKind.MustBeOnDispatcher);

            var handler = Starting;
            if(handler == null)
                return;

            var callbacks = handler.GetInvocationList();
            if(callbacks.Length <= 0)
                return;

            foreach(var callback in callbacks)
            {
                try
                {
                    Debug.Assert(callback != null, "callback != null");
                    callback.DynamicInvoke(this, EventArgs.Empty);
                }
                catch(Exception ex)
                {
                    ErrorManager.OnUnhandledException(ex);
                }
            }
        }

        /// <summary>
        ///     Occurs when the game has started to play.
        /// </summary>
        public event EventHandler Started;

        /// <summary>
        ///     Called after the <see cref="IsPlaying" /> property changes to <see langword="true" /> and after the resources are
        ///     notified.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        protected void OnStarted()
        {
            Dispatcher.ValidateDispatcher(DispatcherValidationKind.MustBeOnDispatcher);

            var handler = Started;
            if(handler == null)
                return;

            var callbacks = handler.GetInvocationList();
            if(callbacks.Length <= 0)
                return;

            foreach(var callback in callbacks)
            {
                try
                {
                    Debug.Assert(callback != null, "callback != null");
                    callback.DynamicInvoke(this, EventArgs.Empty);
                }
                catch(Exception ex)
                {
                    ErrorManager.OnUnhandledException(ex);
                }
            }
        }

        /// <summary>
        ///     Switches the game out of playing mode.
        /// </summary>
        /// <remarks>
        ///     <para>Calling this method will stop the controllers and synchronizers that drive the game.</para>
        ///     <para>
        ///         When this method is called the <see cref="Stopping" /> and <see cref="Stopped" /> events and
        ///         <see cref="INotifyPropertyChanged.PropertyChanged" /> event will fire.
        ///     </para>
        ///     <para>Calling this method will have no effect if the game is already playing</para>
        ///     .
        /// </remarks>
        /// .
        public void Stop()
        {
            Debug.WriteLine("Stopping: " + Name);

            _lock.EnterUpgradeableReadLock();
            try
            {
                if(!_isPlaying)
                    return;

                _lock.EnterWriteLock();
                try
                {
                    _isPlaying = false;
                }
                finally
                {
                    _lock.ExitWriteLock();
                }
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();
            }

            Dispatcher.Invoke(() =>
                              {
                                  OnStopping();
                                  StopResources();
                                  OnPropertyChanged("IsPlaying");
                                  OnStopped();
                              });

            Debug.WriteLine("Stopped: " + Name);
        }

        /// <summary>
        ///     Stops the resources that require triggering.
        /// </summary>
        private void StopResources()
        {
            Dispatcher.ValidateDispatcher(DispatcherValidationKind.MustBeOnDispatcher);

            foreach(var synchronizer in _cues.ToArray())
            {
                Debug.Assert(synchronizer != null, "Cue != null");
                synchronizer.Stop();
            }
        }

        /// <summary>
        ///     Occurs when the game is stopping to play.
        /// </summary>
        public event EventHandler Stopping;

        /// <summary>
        ///     Called before the <see cref="IsPlaying" /> property changes to <see langword="false" /> and before the resources
        ///     are notified.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        protected void OnStopping()
        {
            Dispatcher.ValidateDispatcher(DispatcherValidationKind.MustBeOnDispatcher);

            var handler = Stopping;
            if(handler == null)
                return;

            var callbacks = handler.GetInvocationList();
            if(callbacks.Length <= 0)
                return;

            foreach(var callback in callbacks)
            {
                try
                {
                    Debug.Assert(callback != null, "callback != null");
                    callback.DynamicInvoke(this, EventArgs.Empty);
                }
                catch(Exception ex)
                {
                    ErrorManager.OnUnhandledException(ex);
                }
            }
        }

        /// <summary>
        ///     Occurs when the game has stopped playing.
        /// </summary>
        public event EventHandler Stopped;

        /// <summary>
        ///     Called after the <see cref="IsPlaying" /> property changes to <see langword="true" /> and the resources are
        ///     notified.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        protected void OnStopped()
        {
            Dispatcher.ValidateDispatcher(DispatcherValidationKind.MustBeOnDispatcher);

            var handler = Stopped;
            if(handler == null)
                return;

            var callbacks = handler.GetInvocationList();
            if(callbacks.Length <= 0)
                return;

            foreach(var callback in callbacks)
            {
                try
                {
                    Debug.Assert(callback != null, "callback != null");
                    callback.DynamicInvoke(this, EventArgs.Empty);
                }
                catch(Exception ex)
                {
                    ErrorManager.OnUnhandledException(ex);
                }
            }
        }

        /// <summary>
        ///     Releases unmanaged and - optionally - managed resources.
        /// </summary>
        /// <param name="disposing">
        ///     <see langword="true" /> to release both managed and unmanaged resources;
        ///     <see langword="false" /> to release only unmanaged resources.
        /// </param>
        protected override void Dispose(bool disposing)
        {
            if(disposing)
            {
                _lock.EnterWriteLock();
                try
                {
                    _layers.Dispose();
                    _modes.Dispose();
                    _pictureSequences.Dispose();
                    _pictures.Dispose();
                    _cues.Dispose();
                }
                finally
                {
                    _lock.ExitWriteLock();
                }
                _lock.Dispose();
            }

            base.Dispose(disposing);
        }

        /// <summary>
        ///     Creates a new mode and adds it to the <see cref="Modes" /> collection.
        /// </summary>
        /// <param name="name">The name that uniquely identifies the new mode.</param>
        /// <returns>The newly created mode.</returns>
        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        [NotNull]
        public Mode AddMode([NotNull] string name)
        {
            var mode = new Mode(this, name);

            Debug.WriteLine("Adding mode: " + name);

            _modes.Add(mode);
            return mode;
        }

        /// <summary>
        ///     Creates a new layer and adds it to the <see cref="Layers" /> collection.
        /// </summary>
        /// <param name="name">The name that uniquely identifies the new layer.</param>
        /// <returns>The newly created layer.</returns>
        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        [NotNull]
        public Layer AddLayer([NotNull] string name)
        {
            var layer = new Layer(this, name);

            Debug.WriteLine("Adding layer: " + name);

            _layers.Add(layer);
            return layer;
        }

        /// <summary>
        ///     Creates a new picture and adds it to the <see cref="Pictures" /> collection.
        /// </summary>
        /// <param name="name">The name that uniquely identifies the new picture.</param>
        /// <param name="image">The image to represented by the picture created.</param>
        /// <returns>The newly created picture.</returns>
        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        [NotNull]
        public Picture AddPicture([NotNull] string name, [NotNull] Image image)
        {
            var picture = new Picture(this, name, image);

            Debug.WriteLine("Adding picture: " + name);

            _pictures.Add(picture);
            return picture;
        }

        /// <summary>
        ///     Occurs immediately before the the mode changes.
        /// </summary>
        public event EventHandler<ModeChangeEventArgs> ModeChanging;

        /// <summary>
        ///     Occurs when the mode has changed.
        /// </summary>
        public event EventHandler<ModeChangeEventArgs> ModeChanged;

        /// <summary>
        ///     Resets the positions of mobile elements to those they had at the start of the game.
        /// </summary>
        public void Reset()
        {
            foreach(var layer in _layers)
            {
                Debug.Assert(layer != null, "layer != null");
                foreach(var sprite in layer.Elements)
                {
                    var block = sprite as IBlock;
                    if(block != null)
                        block.Reset();
                }
            }
        }

        /// <summary>
        ///     Snaps the positions of mobile elements for reset later.
        /// </summary>
        public void Snap()
        {
            foreach(var layer in _layers)
            {
                Debug.Assert(layer != null, "layer != null");
                foreach(var sprite in layer.Elements)
                {
                    var block = sprite as IBlock;
                    if(block != null)
                        block.Snap();
                }
            }
        }
    }

    /// <summary>
    ///     The event arguments supplied to handlers of the <see cref="Game.ModeChanged" /> event.
    /// </summary>
    public class ModeChangeEventArgs : EventArgs
    {
        /// <summary>
        ///     The new mode.
        /// </summary>
        [NotNull]
        private readonly Mode _newMode;

        /// <summary>
        ///     The previous mode.
        /// </summary>
        [CanBeNull]
        private readonly Mode _previousMode;

        /// <summary>
        ///     Initializes a new instance of the <see cref="T:System.EventArgs" /> class.
        /// </summary>
        public ModeChangeEventArgs([CanBeNull] Mode previousMode, [NotNull] Mode newMode)
        {
            if(newMode == null)
                throw new ArgumentNullException("newMode");

            _newMode = newMode;
            _previousMode = previousMode;
        }

        /// <summary>
        ///     The previous mode.
        /// </summary>
        [CanBeNull]
        public Mode PreviousMode
        {
            get { return _previousMode; }
        }

        /// <summary>
        ///     The new mode.
        /// </summary>
        [NotNull]
        public Mode NewMode
        {
            get { return _newMode; }
        }
    }
}